import { EventEmitter } from 'node:events';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import { createFakeCloseEvent, FakeCommand } from '../__fixtures__/fake-command.js';
import { Command } from '../command.js';
import { KillOnSignal } from './kill-on-signal.js';

let commands: Command[];
let controller: KillOnSignal;
let process: EventEmitter;
let abortController: AbortController;
beforeEach(() => {
    process = new EventEmitter();
    commands = [new FakeCommand(), new FakeCommand()];
    abortController = new AbortController();
    controller = new KillOnSignal({ process, abortController });
});

it('returns commands that keep non-close streams from original commands', () => {
    const { commands: newCommands } = controller.handle(commands);
    newCommands.forEach((newCommand, i) => {
        expect(newCommand.close).not.toBe(commands[i].close);
        expect(newCommand.error).toBe(commands[i].error);
        expect(newCommand.stdout).toBe(commands[i].stdout);
        expect(newCommand.stderr).toBe(commands[i].stderr);
    });
});

it('returns commands that map SIGINT to exit code 0', () => {
    const { commands: newCommands } = controller.handle(commands);
    expect(newCommands).not.toBe(commands);
    expect(newCommands).toHaveLength(commands.length);

    const callback = vi.fn();
    newCommands[0].close.subscribe(callback);
    process.emit('SIGINT', 'SIGINT');

    // A fake command's .kill() call won't trigger a close event automatically...
    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));

    expect(callback).not.toHaveBeenCalledWith(expect.objectContaining({ exitCode: 'SIGINT' }));
    expect(callback).toHaveBeenCalledWith(expect.objectContaining({ exitCode: 0 }));
});

it('returns commands that keep non-SIGINT exit codes', () => {
    const { commands: newCommands } = controller.handle(commands);
    expect(newCommands).not.toBe(commands);
    expect(newCommands).toHaveLength(commands.length);

    const callback = vi.fn();
    newCommands[0].close.subscribe(callback);
    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));

    expect(callback).toHaveBeenCalledWith(expect.objectContaining({ exitCode: 1 }));
});

describe.each(['SIGINT', 'SIGTERM', 'SIGHUP'])('on %s', (signal) => {
    it('kills all commands', () => {
        controller.handle(commands);
        process.emit(signal, signal);

        expect(process.listenerCount(signal)).toBe(1);
        expect(commands[0].kill).toHaveBeenCalledWith(signal);
        expect(commands[1].kill).toHaveBeenCalledWith(signal);
    });

    it('sends abort signal', () => {
        controller.handle(commands);
        process.emit(signal, signal);

        expect(abortController.signal.aborted).toBe(true);
    });

    it('removes event listener on finish', () => {
        const { onFinish } = controller.handle(commands);
        onFinish();
        expect(process.listenerCount(signal)).toBe(0);
    });
});
